Add validation for accept request and reply#902
Add validation for accept request and reply#902padelsbach wants to merge 1 commit intowolfSSL:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds stricter validation around SSH service negotiation messages to ensure that only the expected ssh-userauth service is accepted during the service request/accept phase (Fixes F-604).
Changes:
- Validate incoming
MSGID_SERVICE_REQUESTservice name againstssh-userauthand disconnect when unsupported. - Validate incoming
MSGID_SERVICE_ACCEPTservice name againstssh-userauthand fail when unexpected.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/internal.c
Outdated
| /* Check if requested service is 'ssh-userauth' */ | ||
| if (ret == WS_SUCCESS) { | ||
| const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH); | ||
| if (nameUserAuth == NULL || XSTRCMP(name, nameUserAuth) != 0) { |
There was a problem hiding this comment.
XSTRCMP(name, nameUserAuth) compares C strings and can be bypassed with an SSH "string" that contains an embedded NUL (e.g., "ssh-userauth\0junk"). Because GetString() copies bytes and NUL-terminates, XSTRCMP will stop at the first NUL and treat it as a valid match even if the received string length differs. Use a length-checked, binary-safe comparison (e.g., validate nameSz equals the expected length and compare with XMEMCMP, or parse with GetStringRef + NameToId).
| /* Check if requested service is 'ssh-userauth' */ | |
| if (ret == WS_SUCCESS) { | |
| const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH); | |
| if (nameUserAuth == NULL || XSTRCMP(name, nameUserAuth) != 0) { | |
| /* Check if requested service is exactly 'ssh-userauth'. */ | |
| if (ret == WS_SUCCESS) { | |
| const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH); | |
| word32 nameUserAuthSz = 0; | |
| if (nameUserAuth != NULL) { | |
| nameUserAuthSz = (word32)XSTRLEN(nameUserAuth); | |
| } | |
| if (nameUserAuth == NULL || nameSz != nameUserAuthSz || | |
| XMEMCMP(name, nameUserAuth, nameUserAuthSz) != 0) { |
src/internal.c
Outdated
| SendDisconnect(ssh, | ||
| WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE); | ||
| ret = WS_INVALID_STATE_E; |
There was a problem hiding this comment.
The return value of SendDisconnect() is ignored. In non-blocking I/O this can return WS_WANT_WRITE, and overwriting ret with WS_INVALID_STATE_E prevents the caller from retrying the disconnect send, potentially leaving the peer connected without receiving the disconnect. Capture/propagate the SendDisconnect() result (and only return WS_INVALID_STATE_E once the disconnect has been successfully queued/sent).
| SendDisconnect(ssh, | |
| WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE); | |
| ret = WS_INVALID_STATE_E; | |
| ret = SendDisconnect(ssh, | |
| WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE); | |
| if (ret == WS_SUCCESS) { | |
| ret = WS_INVALID_STATE_E; | |
| } |
There was a problem hiding this comment.
This is an error case, we do not want to preserve the connection for things like WANT_WRITE. Added comments.
src/internal.c
Outdated
| /* Check if accepted service is 'ssh-userauth' */ | ||
| if (ret == WS_SUCCESS) { | ||
| const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH); | ||
| if (nameUserAuth == NULL || XSTRCMP(name, nameUserAuth) != 0) { | ||
| WLOG(WS_LOG_DEBUG, "Accepted unexpected service: %s", name); | ||
| ret = WS_INVALID_STATE_E; |
There was a problem hiding this comment.
XSTRCMP(name, nameUserAuth) is not binary-safe for SSH "string" fields. A malicious or non-conforming peer could send an accepted service like "ssh-userauth\0junk" and pass this check because XSTRCMP stops at the embedded NUL. Prefer a length-based comparison (check nameSz against WSTRLEN(nameUserAuth) and use XMEMCMP) or parse via GetStringRef and compare IDs via NameToId.
51f23aa to
e69bdab
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #902
Scan targets checked: wolfssh-bugs, wolfssh-compliance, wolfssh-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
|
|
||
| ret = GetString(name, &nameSz, buf, len, idx); | ||
|
|
||
| /* Requested service must be 'ssh-userauth' */ | ||
| if (ret == WS_SUCCESS) { | ||
| const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH); | ||
| if (nameUserAuth == NULL | ||
| || nameSz != (word32)XSTRLEN(nameUserAuth) | ||
| || XMEMCMP(name, nameUserAuth, nameSz) != 0) { | ||
| WLOG(WS_LOG_DEBUG, "Accepted unexpected service: %s", name); | ||
| ret = WS_INVALID_STATE_E; | ||
| } | ||
| } | ||
|
|
||
| if (ret == WS_SUCCESS) { | ||
| WLOG(WS_LOG_DEBUG, "Accepted service: %s", name); | ||
| ssh->serverState = SERVER_USERAUTH_REQUEST_DONE; |
There was a problem hiding this comment.
🔹 [Low] DoServiceAccept does not send SSH_MSG_DISCONNECT on unexpected service name
Category: Disconnect and error handling violations
The new validation in DoServiceAccept detects when the server accepts an unexpected service (not ssh-userauth) but only sets ret = WS_INVALID_STATE_E without sending SSH_MSG_DISCONNECT. By contrast, the parallel validation added in DoServiceRequest (line 6549) correctly calls SendDisconnect(ssh, WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE) before returning the error. Per RFC 4253 §11.1, an implementation SHOULD send SSH_MSG_DISCONNECT when it detects a protocol violation, rather than simply closing the connection. The asymmetry between the two functions suggests this was an oversight rather than an intentional design choice.
/* Requested service must be 'ssh-userauth' */
if (ret == WS_SUCCESS) {
const char* nameUserAuth = IdToName(ID_SERVICE_USERAUTH);
if (nameUserAuth == NULL
|| nameSz != (word32)XSTRLEN(nameUserAuth)
|| XMEMCMP(name, nameUserAuth, nameSz) != 0) {
WLOG(WS_LOG_DEBUG, "Accepted unexpected service: %s", name);
ret = WS_INVALID_STATE_E;
}
}Recommendation: Add a SendDisconnect call before setting the error, matching the pattern used in DoServiceRequest: (void)SendDisconnect(ssh, WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE); with a reason code of WOLFSSH_DISCONNECT_SERVICE_NOT_AVAILABLE or WOLFSSH_DISCONNECT_PROTOCOL_ERROR.
Fixes F-604